/*
 * BaseRemoteObjectProxy.java   0.0.1    Nov 17, 2009
 * 
 * Copyright (c) 2009 mentalsmash.org
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * The use of the Apache License does not indicate that this project is
 * affiliated with the Apache Software Foundation.
 */
package org.mentalsmash.tazio.net.ril.impl;

import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Set;

import org.mentalsmash.documentation.Concurrency;
import org.mentalsmash.tazio.net.ril.RemoteInvocationDispatcher;
import org.mentalsmash.tazio.net.ril.RemoteMethod;
import org.mentalsmash.tazio.net.ril.RemoteObject;
import org.mentalsmash.tazio.net.ril.RemoteObjectProxy;
import org.mentalsmash.tazio.net.ril.RemoteObjectsRegistry;
import org.mentalsmash.tazio.net.ril.RemoteMethod.InvocationSemantics;
import org.mentalsmash.tazio.net.ril.exceptions.NoSuchRemoteMethodException;
import org.mentalsmash.tazio.net.ril.exceptions.NoSuchRemoteObjectException;
import org.mentalsmash.tazio.net.ril.exceptions.UnavailableRemoteObjectException;
import org.mentalsmash.tazio.commons.TazioRuntimeException;
import org.mentalsmash.tazio.commons.identifiers.NID;
import org.mentalsmash.tazio.commons.identifiers.OID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.inject.internal.ImmutableSet;

/**
 * @version 0.0.1 Nov 17, 2009
 * @author Andrea Sorbini <as@mentalsmash.org>
 * 
 */
@Concurrency(concurrentAccess = true, specification = "concurrent access to this class may happen "
	+ "depending on the use clients make of the proxied instance, but any concern about it "
	+ "is left to that same instance. If any problem arises, it might be necessary to synchronize "
	+ "access to the available field, though, since it might read inconsistent values if two "
	+ "invocations occur concurrently and, say, one causes it to be set to false (i.e. receiving a "
	+ "NoSuchRemoteObjectException on invocation) while the other still holds the old value (this"
	+ "shouldn't be a problem because the second incovation would eventually fail and repeat the same "
	+ "steps - i.e. setting available to false - as the first one; moreover, synchronizing this type "
	+ "of access would probably result in a serialization of all invocation on a specific instance and"
	+ "we probably want to avoid that)")
class BaseRemoteObjectProxy implements RemoteObjectProxy {
    private static final Logger log = LoggerFactory
	    .getLogger(RemoteObjectProxy.class);

    private static final long serialVersionUID = -7839105367285436500L;

    private final OID oid;
    private final NID host;
    private final Set<RemoteMethod> methods;

    private boolean available = true;

    private transient RemoteObjectsRegistry registry;
    private transient RemoteInvocationDispatcher dispatcher;

    private final RemoteObject remoteObject;

    /**
     * Creates a new proxy for the instance identified by the specified
     * {@link OID}, providing the specified {@link RemoteMethod}s.
     * 
     * @param oid
     * @param obj
     */
    BaseRemoteObjectProxy(OID oid, NID host, Object obj) {
	this.oid = oid;
	this.host = host;
	RemoteMethod[] methods = BaseRemoteObjectProxy.getObjectMethods(obj);

	this.methods = ImmutableSet.of(methods);


	this.remoteObject = new RemoteObjectImpl(this.oid, this.host, methods, this);
    }

    /**
     * Sets the registry instance to be used by this proxy for local
     * invocations.
     * 
     * @param registry
     */
    public void setRegistry(RemoteObjectsRegistry registry) {
	this.registry = registry;
    }

    /**
     * Sets the dispatcher instance to be used by this proxy for remote
     * invocations.
     * 
     * @param dispatcher
     */
    public void setDispatcher(RemoteInvocationDispatcher dispatcher) {
	this.dispatcher = dispatcher;
    }

    /**
     * Implementation of
     * {@link InvocationHandler#invoke(Object, Method, Object[])}.<br/>
     * It uses {@link RemoteObjectProxy#invoke(RemoteMethod, Object[])} to
     * actually carry out the method invocation.
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
	    throws Throwable {

	if (method.getDeclaringClass().equals(RemoteObject.class)) {
	    Method m = RemoteObjectImpl.class.getMethod(method.getName(),
		    method.getParameterTypes());
	    Object result = m.invoke(this.remoteObject, args);
	    return result;
	}
	
	Method equals = Object.class.getMethod("equals", new Class<?>[]{Object.class});
	Method hashCode = Object.class.getMethod("hashCode", new Class<?>[]{});
	Method toString = Object.class.getMethod("toString", new Class<?>[]{});
	
	if (method.equals(equals)||method.equals(hashCode)||method.equals(toString)){
	    Method m = RemoteObjectImpl.class.getMethod(method.getName(),
		    method.getParameterTypes());
	    Object result = m.invoke(this.remoteObject, args);
	    return result;
	}

	RemoteMethod rm = new RemoteMethod(method);

	if (!this.methods.contains(rm)) {
	    throw new TazioRuntimeException("No method " + rm + " on "
		    + this.oid);
	}

	return this.invoke(rm, args);
    }

    /**
     * Invokes a method with the supplied arguments on the instance handled by
     * this proxy. The invocation is handled locally if the instance identified
     * by this proxy's OID is present in the local registry. Otherwise it is
     * remotely dispatched using the {@link RemoteInvocationDispatcher}.
     * 
     * @param method
     *            the method to invoke
     * @param args
     *            the arguments to invoke the method with
     * @return the result of the method invocation
     * @throws Exception
     *             if any was raised during method's invocation
     */
    public Object invoke(RemoteMethod method, Object[] args) throws Exception {

	if (!this.available) {
	    throw new UnavailableRemoteObjectException(this.oid);
	}

	if (!this.methods.contains(method)) {
	    throw new NoSuchRemoteMethodException(this.oid, method);
	}

	Object result = null;

	boolean onGroup = method.getSemantics().equals(
		InvocationSemantics.OnGroup);

	if (onGroup) {
	    if (this.isLocal()) {
		if (!method.isByCopy()) {
		    args = this.checkArgumentsForRemoteObjects(args);
		}
		result = this.dispatcher.invokeRemoteMethod(this.oid, method,
			args);
	    } else {
		log.error("Requested onGroup invocation but remote object "
			+ this.oid + " isn't available in the local registry");

		this.available = false;

		throw new UnavailableRemoteObjectException(this.oid);
	    }

	} else {
	    try {
		if (this.isLocal()) {
		    result = this.registry.invokeMethod(this.oid, method, args);
		} else {
		    if (!method.isByCopy()) {
			args = this.checkArgumentsForRemoteObjects(args);
		    }
		    result = this.dispatcher.invokeRemoteMethod(this.oid,
			    method, args);
		}
	    } catch (NoSuchRemoteObjectException ex) {
		log.warn("Object " + this.oid + " no longer available");
		this.available = false;
		throw new UnavailableRemoteObjectException(this.oid);
	    }
	}

	if (result instanceof Exception) {
	    if (result instanceof NoSuchRemoteObjectException) {
		log.error("Remote object " + this.oid
			+ " is no longer available");

		this.available = false;

		throw new UnavailableRemoteObjectException(this.oid);
	    }

	    if (result instanceof RuntimeException) {
		throw (RuntimeException) result;
	    }

	    for (Class<?> excType : method.getExceptionTypes()) {
		if (excType.isInstance(result)) {
		    throw (Exception) result;
		}
	    }

	    log.error("Received unknown exception type : "
		    + result.getClass().getCanonicalName() + " (message: "
		    + ((Exception) result).getMessage() + ")");

	    throw new TazioRuntimeException((Exception) result);
	}

	return result;

    }

    private boolean isLocal() {
	return this.registry.hasObject(this.oid);
    }

    protected class RemoteObjectImpl implements RemoteObject {
	private static final long serialVersionUID = 2504961268330616220L;

	private final RemoteMethod[] methods;
	private final OID oid;
	private final BaseRemoteObjectProxy proxy;
	private final NID host;

	public RemoteObjectImpl(OID oid, NID host, RemoteMethod[] methods,
		BaseRemoteObjectProxy proxy) {
	    this.methods = methods;
	    this.oid = oid;
	    this.host = host;
	    this.proxy = proxy;
	}
	
	@Override
	public NID getHostNID(){
	    return this.host;
	}

	/**
	 * @see org.mentalsmash.tazio.net.ril.RemoteObject#getMethods()
	 */
	@Override
	public RemoteMethod[] getMethods() {
	    return this.methods;
	}

	/**
	 * @see org.mentalsmash.tazio.net.ril.RemoteObject#getOID()
	 */
	@Override
	public OID getOID() {
	    return this.oid;
	}

	/**
	 * @see org.mentalsmash.tazio.net.ril.RemoteObject#isLocal()
	 */
	@Override
	public boolean isLocal() {
	    return this.proxy.isLocal();
	}

	/**
	 * @see org.mentalsmash.tazio.net.ril.RemoteObject#getProxy()
	 */
	public RemoteObjectProxy getProxy() {
	    return this.proxy;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
	    final int prime = 31;
	    int result = 1;
	    result = prime * result + ((oid == null) ? 0 : oid.hashCode());
	    return result;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
	    if (this == obj)
		return true;
	    if (obj == null)
		return false;
	    if (!(obj instanceof RemoteObject))
		return false;
	    RemoteObject other = (RemoteObject) obj;
	    if (oid == null) {
		if (other.getOID() != null)
		    return false;
	    } else if (!oid.equals(other.getOID()))
		return false;
	    return true;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.mentalsmash.tazio.net.ril.RemoteObject#isAvailable()
	 */
	@Override
	public boolean isAvailable() {
	    return this.proxy.available;
	}
	
	@Override
	public String toString(){
	    return "<RemoteObject: "+this.oid+">";
	}
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.mentalsmash.tazio.net.ril.RemoteObjectProxy#invokeLocally(org.mentalsmash
     * .tazio.net.ril.RemoteMethod, java.io.Serializable[])
     */
    @Override
    public Object invokeLocally(RemoteMethod method, Serializable[] args)
	    throws NoSuchRemoteObjectException {

	return this.registry.invokeMethod(this.oid, method, args);

    }

    protected static RemoteMethod[] getObjectMethods(Object obj) {
	ArrayList<RemoteMethod> methods = new ArrayList<RemoteMethod>();

	for (Method m : Object.class.getMethods()) {
	    RemoteMethod rm = new RemoteMethod(m);
	    methods.add(rm);
	}

	for (Class<?> intf : getObjectInterfaces(obj)) {
	    for (Method m : intf.getMethods()) {
		RemoteMethod rm = new RemoteMethod(m);
		if (!methods.contains(rm)) {
		    methods.add(rm);
		}
	    }
	}

	return methods.toArray(new RemoteMethod[0]);
    }

    protected static Class<?>[] getObjectInterfaces(Object obj) {
	ArrayList<Class<?>> interfaces = new ArrayList<Class<?>>();

	interfaces.add(RemoteObject.class);

	for (Class<?> c : obj.getClass().getInterfaces()) {
	    interfaces.add(c);
	}

	return interfaces.toArray(new Class<?>[0]);
    }

    protected Object[] checkArgumentsForRemoteObjects(Object[] args) {
	if (args != null) {
	    // RemoteObjectsRegistry registry = this.invoker.getRegistry();

	    ArrayList<Object> arguments = new ArrayList<Object>();

	    for (Object o : args) {

		OID argOID = registry.getOID(o);

		if (argOID != null) {

		    try {
			RemoteObject remoteObj = registry
				.getRemoteObject(argOID);
			arguments.add(remoteObj);
		    } catch (NoSuchRemoteObjectException ex) {
			// should never happen, since we've checked object
			// presence with getOID()
		    }

		} else {
		    arguments.add(o);
		}

	    }

	    return arguments.toArray();

	} else {
	    return new Object[0];
	}
    }

}
